﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Preactor.ObjectModel;
using Preactor.ObjectModel.BaseTypes;
using Preactor.ObjectModel.Entities;
using Preactor.ObjectModel.Interfaces;
using Preactor.ObjectModel.Utilities;
using Preactor.ObjectModelExtensions.Entities;

namespace Preactor.ObjectModelExtensions
{
    public static class OperationExtensions
    {
        public static void HighlightOperation<T>(this T operation) where T : PreactorRecord, IOperation
        {
            IPlanningBoard pb = operation.PreactorObjectModel.PlanningBoard;

            pb.SetOperationLocateState(operation.RecordNumber, true);
            pb.SetLocateState(true);
        }

        public static void HighlightOperations<T>(this IEnumerable<T> operations, int durationInMilliseconds) where T : PreactorRecord, IOperation
        {
            Debug.Assert(durationInMilliseconds >= 0, "durationInMillisecnds must be equal or greater than 0");

            if (operations == null)
            {
                throw new NullReferenceException("Operations is null");
            }

            if (operations.IsEmpty())
            {
                return;
            }

            IPlanningBoard pb = operations.First().PreactorObjectModel.PlanningBoard;

            foreach (T op in operations)
            {
                pb.SetOperationLocateState(op.RecordNumber, true);

                if (durationInMilliseconds == 0)
                {
                    continue;
                }

                Thread.Sleep(durationInMilliseconds);
                pb.SetLocateState(true);
            }

            pb.SetLocateState(true);
        }

        public static void HighlightOperations<T>(this IEnumerable<T> operations) where T : PreactorRecord, IOperation
        {
            operations.HighlightOperations(200);
        }

        public static bool PutOnResource<T>(this T operation, string resourceName, DateTime startTime) where T : PreactorRecord, IOperation
        {
            var pom = operation.PreactorObjectModel;
            var resource = pom.GetRecordByName<IResource>(resourceName) as Resource;

            return resource != null && pom.PlanningBoard.PutOperationOnResource(operation.RecordNumber, resource.RecordNumber, startTime);
        }

        public static bool QuickPutOnResource<T>(this T operation, string resourceName, OperationTimes operationTimes) where T : PreactorRecord, IOperation
        {
            var pom = operation.PreactorObjectModel;
            var resource = pom.GetRecordByName<IResource>(resourceName) as Resource;

            return resource != null && pom.PlanningBoard.QuickPutOperationOnResource(operation.RecordNumber, resource.RecordNumber, operationTimes);
        }

        /// <summary>
        /// Schedule an operation based on the best available resource.
        /// i.e the resource that will finish the job fastest.
        /// If the start time is not provided the operation will be scheduled at the next available start time.
        /// i.e after the last operation which is currently on the resource
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="operation"></param>
        /// <param name="startTime"></param>
        /// <returns></returns>
        public static bool Schedule<T>(this T operation, DateTime startTime = default(DateTime)) where T : PreactorRecord, IOperation
        {
            PreactorObjectModel pom = operation.PreactorObjectModel;

            var resourceGroupName = operation.GetFieldValue<string>( "Resource Group" );
            // var requiredResourceNumber = operation.GetFieldValue<int>("Required Resource"); // Must make sure Required Resource does not have a FREE FORMAT classification.
            var requiredResourceName = operation.GetFieldValue<string>("Required Resource");

            //-----------------------------------------------------------------
            // We have a required resource.

            if (requiredResourceName != string.Empty && requiredResourceName != "Select from Group")
            {
                if (startTime == default(DateTime))
                {
                    var resource = pom.GetResourceByName(requiredResourceName);
                    startTime = resource.IsFinite ? pom.GetNextAvailableStartTime(requiredResourceName) : pom.GetTerminatorTime();
                }

                var operationTimes = operation.TestOperationOnResource(requiredResourceName, startTime);

                if (operationTimes.HasValue)
                {
                    return operation.QuickSchedule(requiredResourceName, operationTimes.Value);
                }

                return false;
            }

            //-----------------------------------------------------------------
            // We don't have a required resource

            var resourceGroup = pom.GetResourceGroupByName(resourceGroupName);
            var resourceNames = resourceGroup.ResourceNames.ToList();

            if (requiredResourceName == string.Empty && !resourceNames.Any())
            {
                throw new InvalidOperationException(String.Format("No resources are associated with the resource group: {0}", resourceGroupName));
            }

            var earliestEndTime = new DateTime();
            var earliestOperationTimes = new OperationTimes();
            string bestResource = String.Empty;

            bool getNextAvailableStartTime = startTime == default(DateTime);

            foreach (var resourceName in resourceNames)
            {
                if (getNextAvailableStartTime)
                {
                    var resource = pom.GetResourceByName(resourceName);
                    startTime = resource.IsFinite ? pom.GetNextAvailableStartTime(resourceName) : pom.GetTerminatorTime();
                }

                var operationTimes = operation.TestOperationOnResource(resourceName, startTime);

                if (!operationTimes.HasValue)
                {
                    continue;
                }

                if (bestResource != String.Empty && operationTimes.Value.ProcessEnd >= earliestEndTime)
                {
                    continue;
                }

                earliestEndTime = operationTimes.Value.ProcessEnd;
                bestResource = resourceName;
                earliestOperationTimes = operationTimes.Value;
            }

            if (bestResource == String.Empty)
            {
                // TestOperationOnResource did not find a resource to schedule the operation.
                // throw new InvalidOperationException(string.Format("A start time could not be found to schedule the operation: {0}", operation));
                return false;
            }

            return operation.QuickSchedule(bestResource, earliestOperationTimes);
        }

        public static OperationTimes? TestOperationOnResource<T>(this T operation, string resourceName, DateTime startTime) where T : PreactorRecord, IOperation
        {
            var planningBoard = operation.PreactorObjectModel.PlanningBoard;
            return planningBoard.TestOperationOnResource( operation.RecordNumber, planningBoard.GetResourceNumber( resourceName ), startTime );
        }
        
        public static void SetBatchTimeDuration<T>(this T operation, TimeSpan duration) where T : PreactorRecord, IOperation
        {
            // TODO: Make this more generic.. SetDuration() which accepts an enum that defines how this is achieved, either by changing the batch time, or quantity or rate of production etc...
            
            // Check that the operation is scheduled
            if (operation.IsScheduled == false)
            {
                // Double check
                if(operation.ResourceNameValue == -1)
                {
                    throw new ArgumentException(String.Format("The operation {0} is not scheduled. Cannot change its batch time." + "\r\nPlease schedule the operation first.", operation));
                }
            }

            if (duration.TotalSeconds < 0)
            {
                throw new ArgumentOutOfRangeException(String.Format("Duration for operation {0} can't be negative.", operation));
            }

            // NOTE: "Batch Time" and "Process Time Type" must be defined in the tdf. Probably better to use a classification.
            operation.SetFieldValue("Process Time Type", "Time Per Batch");
            operation.SetFieldValue("Batch Time", duration);
            operation.EvaluateRecord();

            var resourceName = operation.ResourceName;
            var operationTimes = new OperationTimes(operation.SetupStartTime, operation.StartTime, operation.StartTime.Add(duration));

            operation.Unschedule();
            operation.QuickSchedule(resourceName, operationTimes);
        }

        /// <summary>
        /// You normally can not set the end time of an operation directly (unless you have "Use Actual Times" switched on)
        /// Instead you have to change the quantity or processing time of an operation to inderectly change its 
        /// end time.
        /// </summary>
        /// <param name="operation"></param>
        /// <param name="endTime"></param>
        /// <param name="processType"></param>
        public static void SetEndTime<T>(this T operation, DateTime endTime, ProcessTimeType processType) where T : PreactorRecord, IOperation
        {
            IPlanningBoard pb = operation.PreactorObjectModel.PlanningBoard;

            if (!operation.IsScheduled)
            {
                throw new ArgumentException(String.Format("The operation: {0} must be schedule, before setting the end time.", operation));
            }

            switch (processType)
            {
                    // TODO: Add the other possibilities for the ProcessTimeType enums.
                case ProcessTimeType.TimePerBatch:

                    // We have to find all the calendar states between the start time and the end time.
                    TimeSpan shiftPatternDuration = pb.GetShiftPatternDuration(operation.ResourceName, operation.SetupStartTime, endTime);
                    operation.SetBatchTimeDuration(shiftPatternDuration);

                    break;
                
                default:
                    break;
            }
        }

        /// <summary>
        /// Will split the current operation at the given quantity.
        /// Returns a new operation with the remainder.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="operation"></param>
        /// <param name="quantity"></param>
        /// <returns></returns>
        public static T SplitOperation<T>(this T operation, double quantity) where T : PreactorRecord, IOperation
        {
            PreactorObjectModel pom = operation.PreactorObjectModel;

            T newOp = operation.DuplicateOperation();

            if (quantity >= operation.Quantity)
            {
                throw new ArgumentException(String.Format("The 'quantity' value is greater than the 'operation.Quantity' value, for operation: {0}", operation));
            }

            // There are two different ways of splitting an operation depending on the configuration that is being used.
            // If the configuration uses a Products table then the first portion of code will be executed.
            // If the configuratin uses pegging then the second portion of code will be executed.
            if (pom.ResolveType<IProduct>() != null)
            {
                // We have a Products table
                double operationQuantity = operation.Quantity;

                // ADAM: Should we make the fields below part of the operation Interface?

                // operation.Set("BatchingMethod", "Split");
                operation.SetValue("LotSize", quantity);
                operation.SetValue("Quantity", quantity);

                // newOp.Set("BatchingMethod", "Split");
                newOp.SetValue("LotSize", operationQuantity - quantity);
                newOp.SetValue("Quantity", operationQuantity - quantity);
                newOp.SetValue("LotNumber", operation.LotNumber + 1);
            }

            // else (we do not have a products table)
            return newOp;
        }

        public static IEnumerable<T> Window<T>(this IEnumerable<T> source, DateTime startTime, DateTime endTime) where T : IOperation
        {
            return source.Where(x => (startTime > x.SetupStartTime && startTime < x.EndTime)
                                      || (endTime < x.EndTime && endTime > x.SetupStartTime)
                                      || (startTime <= x.SetupStartTime && endTime >= x.EndTime));
        }

        public static IEnumerable<T> Window<T>(this IEnumerable<T> source, DateTime startTime, DateTime endTime, WindowFilter filter) where T : IOperation
        {
            switch (filter)
            {
                case WindowFilter.StartsIn:
                    {
                        return source.Where(x => (startTime <= x.SetupStartTime && endTime >= x.SetupStartTime));
                    }

                case WindowFilter.EndsIn:
                    {
                        return source.Where(x => (startTime <= x.EndTime) && (endTime >= x.EndTime));
                    }

                case WindowFilter.StartsAndEndsIn:
                    {
                        return source.Where(x => (startTime <= x.SetupStartTime && endTime >= x.SetupStartTime) && (endTime >= x.EndTime));
                    }

                case WindowFilter.StartInEndsAfter:
                    {
                        return source.Where(x => (startTime <= x.SetupStartTime && endTime >= x.SetupStartTime) && (endTime < x.EndTime));
                    }

                case WindowFilter.StartBeforeEndsAfter:
                    {
                        return source.Where(x => (startTime >= x.SetupStartTime) && (endTime < x.EndTime));
                    }

                case WindowFilter.StartsBeforeEndsIn:
                    {
                        return source.Where(x => (startTime >= x.SetupStartTime) && (startTime <= x.EndTime && endTime >= x.EndTime));
                    }

                default:
                    return Window(source, startTime, endTime);
            }
        }

        private static T DuplicateOperation<T>(this T operation) where T : PreactorRecord, IOperation
        {
            var pom = operation.PreactorObjectModel;
            IPreactor preactor = pom.Preactor;

            int orderFormatNumber = preactor.GetFormatNumber(operation.FormatName);
            int newRecordNumber = preactor.CreateRecord(orderFormatNumber);

            // Check the new record was created
            if (newRecordNumber > 0)
            {
                var duplicateOperation = operation.PreactorObjectModel.GetRecord<IOperation>(newRecordNumber) as T;
                double id = duplicateOperation.Id.Value;

                preactor.CopyRecord(orderFormatNumber, operation.RecordNumber, newRecordNumber);

                int test = preactor.ReadFieldInt("Orders", "Number", operation.RecordNumber);
                int testNew = preactor.ReadFieldInt("Orders", "Number", newRecordNumber);

                // Preactor will copy the old primary key value over the new one
                // so reset it to the original value.
                // We have to use preactor's API since the primary key has changed after using the CopyRecord.
                preactor.WriteField(pom.GetFormatFieldPair<IOperation>(x => x.Id), newRecordNumber, id);

                return duplicateOperation;
            }

            throw new InvalidOperationException("Record could not be created.");
        }

        public static void AddToSecondaryConstraint<T>(this T operation, string secondaryConstraintName, ConstraintUsage constraintUsage, int stepSize) where T : PreactorRecord, IOperation
        {
            operation.AddToSecondaryConstraint(secondaryConstraintName, constraintUsage, operation.Quantity, stepSize);
        }

        public static void AddToSecondaryConstraint<T>(this T operation, string secondaryConstraintName, ConstraintUsage constraintUsage, double quantity, int stepSize) where T : PreactorRecord, IOperation
        {
            var pom = operation.PreactorObjectModel;
            var ordersFormatNumber = operation.FormatNumber;
            var recordNumber = operation.RecordNumber;

            var size = pom.Preactor.MatrixFieldSize(ordersFormatNumber, "Secondary Constraints", recordNumber);
            var newIndex = size.X + 1;

            pom.Preactor.SetAutoListSize(ordersFormatNumber, "Secondary Constraints", recordNumber, newIndex);

            pom.Preactor.WriteListField(ordersFormatNumber, "Secondary Constraints", recordNumber, secondaryConstraintName, newIndex);
            pom.Preactor.WriteListField(ordersFormatNumber, "Constraint Usage", recordNumber, constraintUsage.Value, newIndex);
            pom.Preactor.WriteListField(ordersFormatNumber, "Constraint Step Size", recordNumber, stepSize, newIndex);
            pom.Preactor.WriteListField(ordersFormatNumber, "Constraint Quantity", recordNumber, quantity, newIndex);
        }
    }
}